Análise das podas do AQE¶

Nesta análise vamos experimentar diferentes abordagens de poda para o AQE, de forma a evitar que a expansão seja muito longa e acabe por prejudicar as consultas do Elasticsearch.

Carregando libs¶

In [1]:
from itertools import product
import json
import yaml
import pandas as pd
import plotly.express as px

from utils.utils import get_expanded_queries, make_elasticsearch_new_aqe_queries,\
    create_new_expanded_queries, create_new_aqe_validation_dataset, create_new_aqe_metrics,\
    expanded_with_aqe_boost_order, expanded_with_aqe_order

Carregando as configurações e bases dados¶

In [2]:
with open("../conf/config.yaml", "r") as yamlfile:
    cfg = yaml.safe_load(yamlfile)
In [3]:
with open("../../dados/regis/regis_queries.json", 'r') as regis_file:
    regis_queries = json.load(regis_file)
In [4]:
regis_queries = get_expanded_queries(regis_queries)
regis_queries[:2]
Out[4]:
[{'title': 'História da geoquímica na Petrobras',
  'query_id': 'Q1',
  'expanded_query': '((História da geoquímica na Petrobras) OR ( (historia^1.000 OR história^0.890 OR history^0.571 OR "histórico do campo"^0.525 OR review^0.159 OR revisão^0.164 OR "histórico de caso"^0.225) OR (geoquimica^1.000 OR geoquímica^0.890 OR geoquímicas^0.691 OR geoquímico^0.672 OR geoquimicos^0.736 OR geoquimicas^0.691 OR geoquimico^0.672 OR geochemistry^0.601 OR "geochemical anomaly"^0.225 OR "geochemical interpretation"^0.225 OR "composição dos sedimentos"^0.225 OR "sediment composition"^0.225 OR geology^0.195 OR geologia^0.253 OR petrochemistry^0.153 OR petroquímica^0.184 OR "geochemical cycle"^0.225 OR petrografia^0.255 OR petrography^0.191 OR "análise de rochas"^0.225 OR "rock analysis"^0.225 OR "composição das rochas"^0.225 OR "rock composition"^0.225 OR transect^0.150 OR "geochemical map"^0.225 OR "geochemical exploration"^0.225 OR "geochemical logging"^0.225 OR geophysics^0.179 OR geofísica^0.256 OR "geochemical data"^0.225) OR petrobras^1.000 ))'},
 {'title': 'Lógica fuzzy aplicada  à industria do petróleo',
  'query_id': 'Q2',
  'expanded_query': '((Lógica fuzzy aplicada à industria do petróleo) OR ( ("logica fuzzy"^1.000 OR "lógica fuzzy"^0.667 OR "lógica difusa"^0.667 OR "logica nebulosa"^0.667 OR "logica difusa"^0.667 OR "fuzzy logic"^0.667) OR ("industria do petroleo"^1.000 OR "indústria do petróleo"^0.667 OR "industria de petroleo"^0.667 OR "petroleum industry"^0.667) OR (logica^1.000 OR lógica^0.890 OR lógico^0.744) OR fuzzy^1.000 OR (aplicada^1.000 OR aplicado^0.795 OR aplicados^0.756) OR (industria^1.000 OR indústria^0.890 OR industry^0.576) OR (petroleo^1.000 OR petróleo^0.890 OR petróleos^0.705 OR petroleos^0.705 OR petroleum^0.632 OR "gasolina natural"^0.525 OR "natural gas"^0.350 OR "gás natural"^0.423 OR "commingled production"^0.525 OR "produção misturada"^0.525 OR "óleo cru"^0.525 OR "crude oil"^0.353 OR "hydrocarbon potential"^0.225 OR "potencial de hidrocarbonetos"^0.225 OR "fluido do reservatório"^0.225 OR "reservoir fluid"^0.225 OR "raw material"^0.225 OR "matéria prima"^0.224 OR "razão de hidrocarbonetos"^0.225 OR "hydrocarbon ratio"^0.225 OR "fossil fuel"^0.150 OR "combustível fóssil"^0.225) ))'}]
In [5]:
ground_truth = pd.read_csv(
    "../../dados/regis/regis_ground_truth.csv"
).rename(
    columns={"relevance": "relevance_ground_truth"}
)
ground_truth.head()
Out[5]:
query_id document_id relevance_ground_truth
0 Q1 BR-BG.03944 1
1 Q1 BR-BG.03925 1
2 Q1 BR-TU.23384 0
3 Q1 BR-TU.12209 0
4 Q1 BR-BG.04089 2

Criando queries com podas baseadas nos fatores de boosting do AQE¶

Aqui vamos experimentar uma poda com diferentes quantidades de termos utilizando os pesos já estabelecidos pelo AQE.

In [6]:
all_expanded_queries = list()
for query in regis_queries:
    new_expanded_queries = create_new_expanded_queries(query["expanded_query"], expansion=expanded_with_aqe_boost_order)
    for num_termos, new_expanded_query in new_expanded_queries:
        q = query.copy()
        q["expanded_query"] = new_expanded_query
        q["num_termos"] = num_termos
        all_expanded_queries.append(q)
all_expanded_queries[:2]
Out[6]:
[{'title': 'História da geoquímica na Petrobras',
  'query_id': 'Q1',
  'expanded_query': '((História da geoquímica na Petrobras) OR ((historia^0.000 OR história^0.000 OR history^0.000 OR "histórico do campo"^0.000 OR review^0.000 OR revisão^0.000 OR "histórico de caso"^0.000) OR (geoquimica^0.000 OR geoquímica^0.000 OR geoquímicas^0.000 OR geoquímico^0.000 OR geoquimicos^0.000 OR geoquimicas^0.000 OR geoquimico^0.000 OR geochemistry^0.000 OR "geochemical anomaly"^0.000 OR "geochemical interpretation"^0.000 OR "composição dos sedimentos"^0.000 OR "sediment composition"^0.000 OR geology^0.000 OR geologia^0.000 OR petrochemistry^0.000 OR petroquímica^0.000 OR "geochemical cycle"^0.000 OR petrografia^0.000 OR petrography^0.000 OR "análise de rochas"^0.000 OR "rock analysis"^0.000 OR "composição das rochas"^0.000 OR "rock composition"^0.000 OR transect^0.000 OR "geochemical map"^0.000 OR "geochemical exploration"^0.000 OR "geochemical logging"^0.000 OR geophysics^0.000 OR geofísica^0.000 OR "geochemical data"^0.000) OR petrobras^0.000 ))',
  'num_termos': 0},
 {'title': 'História da geoquímica na Petrobras',
  'query_id': 'Q1',
  'expanded_query': '((História da geoquímica na Petrobras) OR ((historia^0.100 OR história^0.000 OR history^0.000 OR "histórico do campo"^0.000 OR review^0.000 OR revisão^0.000 OR "histórico de caso"^0.000) OR (geoquimica^0.100 OR geoquímica^0.000 OR geoquímicas^0.000 OR geoquímico^0.000 OR geoquimicos^0.000 OR geoquimicas^0.000 OR geoquimico^0.000 OR geochemistry^0.000 OR "geochemical anomaly"^0.000 OR "geochemical interpretation"^0.000 OR "composição dos sedimentos"^0.000 OR "sediment composition"^0.000 OR geology^0.000 OR geologia^0.000 OR petrochemistry^0.000 OR petroquímica^0.000 OR "geochemical cycle"^0.000 OR petrografia^0.000 OR petrography^0.000 OR "análise de rochas"^0.000 OR "rock analysis"^0.000 OR "composição das rochas"^0.000 OR "rock composition"^0.000 OR transect^0.000 OR "geochemical map"^0.000 OR "geochemical exploration"^0.000 OR "geochemical logging"^0.000 OR geophysics^0.000 OR geofísica^0.000 OR "geochemical data"^0.000) OR petrobras^0.100 ))',
  'num_termos': 1}]

Realizando consultas no Elasticsearch¶

Em posse das queries que utilizam diferentes quantidades de termos com boosting do elastic search vamos criar o dataset de validação, o qual possui informações do ground truth da base de dados REGIS.

In [7]:
ranking_result_df = make_elasticsearch_new_aqe_queries(
    all_expanded_queries,
    cfg,
    num_docs=24
)
ranking_result_df.head()
Out[7]:
query_id num_termos document_id relevance_ranking
0 Q1 0 BR-BG.03964 9.573541
1 Q1 0 BR-BG.03967 9.460924
2 Q1 0 BR-BG.04004 9.276192
3 Q1 0 BR-TU.20287 9.119863
4 Q1 0 BR-BT.05005 9.103277
In [8]:
validation_dataset = create_new_aqe_validation_dataset(ranking_result_df, ground_truth)
validation_dataset.head()
Out[8]:
query_id num_termos document_id relevance_ranking relevance_ground_truth evaluated
0 Q1 0.0 BR-BG.03964 9.573541 2.0 True
1 Q1 0.0 BR-BG.03967 9.460924 3.0 True
2 Q1 0.0 BR-BG.04004 9.276192 1.0 True
3 Q1 0.0 BR-TU.20287 9.119863 0.0 True
4 Q1 0.0 BR-BT.05005 9.103277 1.0 True

Análise das consultas no Elasticsearch¶

Agora vamos criar as métricas para cada base de dados e quantidade de termos derivados e visualizar os resultados.

Criando métricas¶

In [9]:
metrics_df = create_new_aqe_metrics(validation_dataset)
metrics_df.head()
Out[9]:
query_id num_termos ndcg@24 ap@24 eval_prop
0 Q1 0.0 0.766699 0.355878 0.944444
1 Q1 1.0 0.731755 0.304273 0.886792
2 Q1 2.0 0.732425 0.302159 0.905660
3 Q1 3.0 0.695701 0.290881 0.905660
4 Q1 4.0 0.742415 0.337229 0.927273

Avaliando métricas¶

Vamos agora avaliar as métricas. Vamos utilizar as seguintes métricas:

  • ndcg - Normalized Discounted Cumulative Gain
  • map - Mean Average Precision
  • eval_prop - Proporção de documentos avaliados

Vejamos qual a melhor quantidade de termos derivados para cada query:

In [10]:
data_viz = metrics_df.groupby(
    "query_id"
).agg({
    "ndcg@24": "max"
}).reset_index(
).merge(
    metrics_df, how="left", on=["query_id", "ndcg@24"]
).sort_values(
    ["query_id", "num_termos"]
).drop_duplicates(
    subset="query_id", keep="first"
)
data_viz.head()

fig = px.scatter(
    data_viz,
    x="num_termos",
    y="ndcg@24",
    labels={
        "num_termos": "Número de termos",
        "ndcg@24": "NDCG@24",
    },
    hover_data=["query_id", "num_termos", "ndcg@24"],
    title="Melhor número de termos por query",
    marginal_x="histogram"
)
fig.show()

Podemos ver que a maior concentração está abaixo dos 5 termos derivados.

Vejamos a média para cada número de termos derivados:

In [11]:
queries_boosts_prod = pd.DataFrame(
    product(metrics_df["query_id"].unique(), metrics_df["num_termos"].unique()),
    columns=["query_id", "num_termos"]
)

data_viz = queries_boosts_prod.merge(
    metrics_df, on=["query_id", "num_termos"], how="left"
).fillna(
    method="ffill"
).groupby(
    "num_termos"
).agg(
    mean_ndcg = ("ndcg@24", "mean")
).reset_index()

fig = px.line(
    data_viz,
    x="num_termos",
    y="mean_ndcg",
    labels={
        "num_termos": "Número de termos",
        "mean_ndcg": "NDCG@24 médio",
    },
    markers=True,
    title="NDCG@24 médio para cada número de termos"
)
fig.show()

Podemos ver que no geral, utilizar 5 termos derivados traz o melhor resultado, o qual é melhor que o Elasticsearch puro.

Vejamos como fica a distribuição dos NDCGs@24 ao utilizar o limiar de poda de 5 termos:

In [12]:
metrics_df_poda = metrics_df.query(
    "num_termos <= 5"
).sort_values(
    ["query_id", "num_termos", "ndcg@24"]
).groupby(
    "query_id"
).last()

data_viz = metrics_df.groupby(
    "query_id"
).agg({
    "ndcg@24": "max"
}).merge(
    metrics_df_poda,
    on="query_id",
    suffixes=(" max", ""),
    how="left"
).reset_index(
).melt(
    id_vars=["query_id"],
    value_vars=["ndcg@24 max", "ndcg@24"],
    var_name="metric"
).sort_values(
    ["metric", "value"], ascending=[True, False]
)

fig = px.bar(
    data_viz,
    x="query_id",
    y="value",
    color="metric",
    barmode='group',
    labels={
        "query_id": "Query ID",
        "value": "NDCG@24",
    },
)
fig.show()

Podemos ver que mais de metade das queries possuem NDCG@24 acima de 0,8. Podemos ver também que apenas quatro das 32 queries tem uma diferença substancial entre o NDCG@24 com cinco termos expandidos e o máximo. São elas: Q19, Q17, Q23 e Q34 .

Vejamos quais as melhores quantidades de termos para essas queries:

In [13]:
queries = ["Q23", "Q34", "Q19", "Q17"]

metrics_df.query(
    "query_id.isin(@queries)"
).groupby(
    "query_id"
).agg({
    "ndcg@24": "max"
}).reset_index(
).merge(
    metrics_df, how="left", on=["query_id", "ndcg@24"]
).sort_values(
    ["query_id", "num_termos"]
).drop_duplicates(
    subset="query_id", keep="first"
)
Out[13]:
query_id ndcg@24 num_termos ap@24 eval_prop
0 Q17 0.560611 22.0 0.500000 0.384615
7 Q19 0.884441 1.0 0.666013 0.656250
8 Q23 0.919277 2.0 0.777877 0.818182
9 Q34 0.784010 0.0 0.361111 0.517241

Podemos ver que exceto a Q17 todos os valores foram próximos de 5. A Q17 parece ser um caso atípico de que algum termo que era considerado de baixa relevância trouxe bons resultados.

Criando queries com podas baseadas na ordem do AQE¶

Aqui vamos experimentar uma poda com diferentes quantidades de termos utilizando a ordem do AQE. A motivação desse experimento é devido ao padrão de fatores de boostings usados, os quais trazem pesos maiores no início e depois vão se misturando, que indica que algum processamento foi realizado.

In [14]:
all_expanded_queries = list()
for query in regis_queries:
    new_expanded_queries = create_new_expanded_queries(query["expanded_query"], expansion=expanded_with_aqe_order)
    for num_termos, new_expanded_query in new_expanded_queries:
        q = query.copy()
        q["expanded_query"] = new_expanded_query
        q["num_termos"] = num_termos
        all_expanded_queries.append(q)
all_expanded_queries[:2]
Out[14]:
[{'title': 'História da geoquímica na Petrobras',
  'query_id': 'Q1',
  'expanded_query': '((História da geoquímica na Petrobras) OR ((historia^0.000 OR história^0.000 OR history^0.000 OR "histórico do campo"^0.000 OR review^0.000 OR revisão^0.000 OR "histórico de caso"^0.000) OR (geoquimica^0.000 OR geoquímica^0.000 OR geoquímicas^0.000 OR geoquímico^0.000 OR geoquimicos^0.000 OR geoquimicas^0.000 OR geoquimico^0.000 OR geochemistry^0.000 OR "geochemical anomaly"^0.000 OR "geochemical interpretation"^0.000 OR "composição dos sedimentos"^0.000 OR "sediment composition"^0.000 OR geology^0.000 OR geologia^0.000 OR petrochemistry^0.000 OR petroquímica^0.000 OR "geochemical cycle"^0.000 OR petrografia^0.000 OR petrography^0.000 OR "análise de rochas"^0.000 OR "rock analysis"^0.000 OR "composição das rochas"^0.000 OR "rock composition"^0.000 OR transect^0.000 OR "geochemical map"^0.000 OR "geochemical exploration"^0.000 OR "geochemical logging"^0.000 OR geophysics^0.000 OR geofísica^0.000 OR "geochemical data"^0.000) OR petrobras^0.000 ))',
  'num_termos': 0},
 {'title': 'História da geoquímica na Petrobras',
  'query_id': 'Q1',
  'expanded_query': '((História da geoquímica na Petrobras) OR ((historia^0.100 OR história^0.000 OR history^0.000 OR "histórico do campo"^0.000 OR review^0.000 OR revisão^0.000 OR "histórico de caso"^0.000) OR (geoquimica^0.100 OR geoquímica^0.000 OR geoquímicas^0.000 OR geoquímico^0.000 OR geoquimicos^0.000 OR geoquimicas^0.000 OR geoquimico^0.000 OR geochemistry^0.000 OR "geochemical anomaly"^0.000 OR "geochemical interpretation"^0.000 OR "composição dos sedimentos"^0.000 OR "sediment composition"^0.000 OR geology^0.000 OR geologia^0.000 OR petrochemistry^0.000 OR petroquímica^0.000 OR "geochemical cycle"^0.000 OR petrografia^0.000 OR petrography^0.000 OR "análise de rochas"^0.000 OR "rock analysis"^0.000 OR "composição das rochas"^0.000 OR "rock composition"^0.000 OR transect^0.000 OR "geochemical map"^0.000 OR "geochemical exploration"^0.000 OR "geochemical logging"^0.000 OR geophysics^0.000 OR geofísica^0.000 OR "geochemical data"^0.000) OR petrobras^0.100 ))',
  'num_termos': 1}]

Realizando consultas no Elasticsearch¶

Em posse das queries que utilizam diferentes quantidades de termos com boosting do elastic search vamos criar o dataset de validação, o qual possui informações do ground truth da base de dados REGIS.

In [15]:
ranking_result_df = make_elasticsearch_new_aqe_queries(
    all_expanded_queries,
    cfg,
    num_docs=24
)
ranking_result_df.head()
Out[15]:
query_id num_termos document_id relevance_ranking
0 Q1 0 BR-BG.03964 9.573541
1 Q1 0 BR-BG.03967 9.460924
2 Q1 0 BR-BG.04004 9.276192
3 Q1 0 BR-TU.20287 9.119863
4 Q1 0 BR-BT.05005 9.103277
In [16]:
validation_dataset = create_new_aqe_validation_dataset(ranking_result_df, ground_truth)
validation_dataset.head()
Out[16]:
query_id num_termos document_id relevance_ranking relevance_ground_truth evaluated
0 Q1 0.0 BR-BG.03964 9.573541 2.0 True
1 Q1 0.0 BR-BG.03967 9.460924 3.0 True
2 Q1 0.0 BR-BG.04004 9.276192 1.0 True
3 Q1 0.0 BR-TU.20287 9.119863 0.0 True
4 Q1 0.0 BR-BT.05005 9.103277 1.0 True

Análise das consultas no Elasticsearch¶

Agora vamos criar as métricas para cada base de dados e quantidade de termos derivados e visualizar os resultados.

Criando métricas¶

In [17]:
metrics_df = create_new_aqe_metrics(validation_dataset)
metrics_df.head()
Out[17]:
query_id num_termos ndcg@24 ap@24 eval_prop
0 Q1 0.0 0.766699 0.355878 0.944444
1 Q1 1.0 0.731755 0.304273 0.886792
2 Q1 2.0 0.732425 0.302159 0.905660
3 Q1 3.0 0.745183 0.337229 0.927273
4 Q1 4.0 0.706277 0.285714 0.928571

Avaliando métricas¶

Vamos agora avaliar as métricas. Vamos utilizar as seguintes métricas:

  • ndcg - Normalized Discounted Cumulative Gain
  • map - Mean Average Precision
  • eval_prop - Proporção de documentos avaliados

Vejamos qual a melhor quantidade de termos derivados para cada query:

In [18]:
data_viz = metrics_df.groupby(
    "query_id"
).agg({
    "ndcg@24": "max"
}).reset_index(
).merge(
    metrics_df, how="left", on=["query_id", "ndcg@24"]
).sort_values(
    ["query_id", "num_termos"]
).drop_duplicates(
    subset="query_id", keep="first"
)
data_viz.head()

fig = px.scatter(
    data_viz,
    x="num_termos",
    y="ndcg@24",
    labels={
        "num_termos": "Número de termos",
        "ndcg@24": "NDCG@24",
    },
    hover_data=["query_id", "num_termos", "ndcg@24"],
    title="Melhor número de termos por query",
    marginal_x="histogram"
)
fig.show()

Podemos ver que a maior concentração está abaixo dos 5 termos derivados.

Vejamos a média para cada número de termos derivados:

In [19]:
queries_boosts_prod = pd.DataFrame(
    product(metrics_df["query_id"].unique(), metrics_df["num_termos"].unique()),
    columns=["query_id", "num_termos"]
)

data_viz = queries_boosts_prod.merge(
    metrics_df, on=["query_id", "num_termos"], how="left"
).fillna(
    method="ffill"
).groupby(
    "num_termos"
).agg(
    mean_ndcg = ("ndcg@24", "mean")
).reset_index()

fig = px.line(
    data_viz,
    x="num_termos",
    y="mean_ndcg",
    labels={
        "num_termos": "Número de termos",
        "mean_ndcg": "NDCG@24 médio",
    },
    markers=True,
    title="NDCG@24 médio para cada número de termos"
)
fig.show()

Podemos ver que utilizar os primeiros 3 termos derivados traz o melhor resultado, o qual é melhor que o Elasticsearch puro.

Vejamos como fica a distribuição dos NDCGs@24 ao utilizar o limiar de poda de 3 termos:

In [20]:
metrics_df_poda = metrics_df.query(
    "num_termos <= 3"
).sort_values(
    ["query_id", "num_termos", "ndcg@24"]
).groupby(
    "query_id"
).last()

data_viz = metrics_df.groupby(
    "query_id"
).agg({
    "ndcg@24": "max"
}).merge(
    metrics_df_poda,
    on="query_id",
    suffixes=(" max", ""),
    how="left"
).reset_index(
).melt(
    id_vars=["query_id"],
    value_vars=["ndcg@24 max", "ndcg@24"],
    var_name="metric"
).sort_values(
    ["metric", "value"], ascending=[True, False]
)

fig = px.bar(
    data_viz,
    x="query_id",
    y="value",
    color="metric",
    barmode='group',
    labels={
        "query_id": "Query ID",
        "value": "NDCG@24",
    },
)
fig.show()

Podemos ver que metade das queries possuem NDCG@24 acima de 0,8. Podemos ver também que apenas quatro das 32 queries tem uma diferença substancial entre o NDCG@24 com os três primeiros termos expandidos e o máximo. São elas: Q17, Q34, Q19 e Q3.

Vejamos quais as melhores quantidades de termos para essas queries:

In [21]:
queries = ["Q17", "Q34", "Q19", "Q3"]

metrics_df.query(
    "query_id.isin(@queries)"
).groupby(
    "query_id"
).agg({
    "ndcg@24": "max"
}).reset_index(
).merge(
    metrics_df, how="left", on=["query_id", "ndcg@24"]
).sort_values(
    ["query_id", "num_termos"]
).drop_duplicates(
    subset="query_id", keep="first"
)
Out[21]:
query_id ndcg@24 num_termos ap@24 eval_prop
0 Q17 0.828641 18.0 1.000000 0.346154
10 Q19 0.884441 1.0 0.666013 0.656250
11 Q3 0.795952 17.0 0.466080 0.615385
14 Q34 0.784010 0.0 0.361111 0.517241

Podemos ver que exceto a Q17 e Q3 todos foram próximos de 3 termos. A Q17 e Q3 parecem ser casos atípicos, em que algum termo que era considerado de baixa relevância trouxe bons resultados.

Conclusão¶

Nesta análise vimos que a poda dos métodos do AQE ajudam a melhorar as métricas de performance. Vimos duas abordagens de poda: utilizar a ordem dos fatores de boosting e a ordem provinda do AQE. Em ambos, os melhores resultados de NDCG@24 foram na casa de 0.76, sendo ambos superiores ao Elasticsearch puro, mas a ordem do AQE trouxe um resultado ligeiramente melhor. Apesar disso, consideramos que utilizar a ordem dos fatores de boosting é o mecanismo de poda mais confiável, pois a variação do NDCG@24 em maiores número de termos é bem menor, tornando um método de poda mais estável. Além disso, podando com até 5 termos, as queries que trazem maiores diferenças apresentam número de termos mais próximos. Logo, concluímos que utilizar a ordem do fator de boosting com até cinco termos é o método de poda mais razoável para o caso geral.